/*
 * Created on May 31, 2005
 */
package org.vectrics.hibernate;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.vectrics.SystemException;
import org.vectrics.company.Company;
import org.vectrics.domain.Domain;
import org.vectrics.event.Event;
import org.vectrics.event.EventAttachment;
import org.vectrics.provider.CoreServiceLocator;
import org.vectrics.user.Person;

/**
 * God class (por "oh my god...").
 *
 * @author mmore
 *
 */
public final class ThreadProperties {
    
    private ThreadProperties() {
        /* Intentionally empty */
    }

	private static final String SESSION_COUNT_MUTEX = "sessionCountMutex324324234";

	private static transient Logger log = Logger.getLogger(ThreadProperties.class);
	private static transient Logger sessionCountLog = Logger.getLogger(ThreadProperties.class.getName() + ".SessionCount");

	private static int openSessionCount = 0;
    private static ThreadLocal<HibernateTransaction> threadTransaction = new ThreadLocal<HibernateTransaction>();
    private static ThreadLocal<Session> hibernateSession = new ThreadLocal<Session>();
    private static ThreadLocal<Person> threadPerson = new ThreadLocal<Person>();
    private static ThreadLocal<Boolean> threadEventsOff = new ThreadLocal<Boolean>();
    private static ThreadLocal<Object> threadCompany = new ThreadLocal<Object>();
    private static ThreadLocal<Domain> threadDomain = new ThreadLocal<Domain>();
    private static ThreadLocal<Boolean> threadSystemUser = new ThreadLocal<Boolean>();
    private static ThreadLocal<Set<Object>> threadEvents = new ThreadLocal<Set<Object>>();
    private static ThreadLocal<ApplicationContext> threadApplicationContext = new ThreadLocal<ApplicationContext>();
    private static ThreadLocal<HibernateUtil> threadHibernateUtil = new ThreadLocal<HibernateUtil>();
    
    public static void clearAll() {
    	threadTransaction.set(null);
    	hibernateSession.set(null);
    	threadPerson.set(null);
    	threadEventsOff.set(null);
    	threadCompany.set(null);
    	threadDomain.set(null);
    	threadSystemUser.set(null);
    	threadEvents.set(null);
    	threadApplicationContext.set(null);
    	threadHibernateUtil.set(null);
    }
    
    public static HibernateUtil getHibernateUtil() {
        HibernateUtil hibernate = threadHibernateUtil.get();
    	
        if (hibernate == null) {
        	hibernate = (HibernateUtil) CoreServiceLocator.findServiceBean("hibernate.util");
        }
        return(hibernate);
    }
    
    public static void setHibernateUtil(HibernateUtil hibernateUtil) {
    	try {
    		threadHibernateUtil.set(hibernateUtil);
    	} catch (Exception ex) {
    		throw new SystemException("Exception setting hibernate util", ex);
    	}
    }

    /**
     * Return Hibernate Session.
     * @return
     */
    public static Session getSession() {
    	Session s = hibernateSession.get();
    	try {
	    	if ((s == null) || (!s.isOpen())) {
	    		log.debug("Creating session for thread: " + Thread.currentThread().getName());
	            s = ThreadProperties.getHibernateUtil().createNewSession();
	    		hibernateSession.set(s);
	    		synchronized (SESSION_COUNT_MUTEX) {
	    			openSessionCount++;
	    			sessionCountLog.debug("+++++++++++++++++++++ Session Created  " + ThreadProperties.openSessionCount);
	    		}
	    	} else {
	    		log.debug("Thread has session already");
	    	}
    	} catch (Exception ex) {
    		log.error("Exception getting session", ex);
    		throw new SystemException("Exception getting session", ex);
    	}
    	return s;
    }

    /**
     * Make certain a session is active.
     */
    public static void assertSession() {
    	Session s = hibernateSession.get();
    	if (s == null)  {
    		throw new SystemException("Session not found");
    	} else if (!s.isOpen()) {
    		throw new SystemException("Session is closed");
    	} else if (!s.isConnected()) {
    		throw new SystemException("Session is not connected during transaction assertion.");
    	} 

    }
    
    public static boolean isTransactionInProgress() {
    	HibernateTransaction tx = threadTransaction.get();
    	boolean result = false;
    	if (tx != null) {
    		if (tx.getTransaction().isActive()) {
    			result = true;
    		}
    	}
    	return(result);
    }
    
    public static void setApplicationContext(ApplicationContext appContext) {
    	if (appContext == null)
    		throw new SystemException("Attempt to set thread's application context to null");
    	try {
    		log.debug("Store app context with thread");
    		threadApplicationContext.set(appContext);
    	} catch (Exception ex) {
    		throw new SystemException("Exception setting bean factory", ex);
    	}
    }
    
    public static Set<Object> getEvents() {
    	Set<Object> events = threadEvents.get();
    	if (events == null) {
    		events = new HashSet<Object>();
    		threadEvents.set(events);
    	}
    	return(events);
    }

	public static String getUriDomainId() {
		String idstr = getDomain().getId().toString();
		while (idstr.length() < 5) {
			idstr = "0" + idstr;
		}
		idstr = "311" + idstr;
		
		return(idstr);
	}

	public static String getUriDomainId(Domain domain) {
		return(getUriDomainId(domain.getId()));
	}
	
	public static String getUriDomainId(Long id) {
		String idstr = id.toString();
		while (idstr.length() < 5) {
			idstr = "0" + idstr;
		}
		idstr = "311" + idstr;
		
		return(idstr);
	}
    
    public static ApplicationContext getApplicationContext() {
    	ApplicationContext beanFactory = threadApplicationContext.get();
    	return beanFactory;
    }

    public static void setPerson(Person authPerson) {
    	try {
    		if (authPerson == null) {
    			log.debug("AuthPerson is null, new threadlocal");
	    		threadPerson.set(Person.NULL_PERSON);
    		} else {
	    		log.debug("Force load of lazy-load roles");
				authPerson.getPrivileges().size();
	    		log.debug("authPerson locale = " + authPerson.getLocale());
	    		
				if (log.isDebugEnabled())
					log.debug("Store auth person with thread: " + authPerson);
	    		threadPerson.set(authPerson);
    		}
    	} catch (Exception ex) {
    		throw new SystemException("Exception setting person: " + ex.getLocalizedMessage(), ex);
    	}
    }

    public static boolean getEventsOff() {
    	Boolean eventsOff = threadEventsOff.get();
    	if (eventsOff == null)
    		eventsOff = new Boolean(false);
    	return(eventsOff.booleanValue());
    }
    
    /**
     * Turn of Event generation when objects are created.  This is useful for creating sample data
     * where event generation can slow down the rapid creation of the sample data.  Creation or modification
     * of various data objects usually causes events to be generated which causes event handling in
     * a batch job.  This method will turn off this event generation.
     * @param value
     */
    public static void setEventsOff(boolean value) {
    	try {
    		if (value) {
    			log.warn("**************************************************************");
    			log.warn("* WARNING - EVENT GENERATION TURNED OFF                      *");
    			log.warn("**************************************************************");
    		}
    		
    		threadEventsOff.set(new Boolean(value));
    	} catch (Exception ex) {
    		throw new SystemException("Exception setting eventsOff: " + ex.getLocalizedMessage(), ex);
    	}
    }
    
    public static Person getPerson() {
    	Person person = threadPerson.get();
    	log.debug("reload person in case new hibernate session started");
    	if (person == Person.NULL_PERSON)
    		person = null;
    	return person;
    }
    
    public static void setSystemUser(boolean value) {
    	try {
    		threadSystemUser.set(new Boolean(value));
    	} catch (Exception ex) {
    		throw new SystemException("Exception setting system user", ex);
    	}
    }
    
    public static boolean getSystemUser() {
    	Boolean result = threadSystemUser.get();
    	if (result != null) {
    		return(result.booleanValue());
    	}
    	return false;
    }

    public static void setDomain(Domain domain) {
    	if (log.isDebugEnabled())
    		log.debug("Thread domain set to: " + domain);
   		threadDomain.set(domain);
    }
    
    public static Domain getDomain() {
    	Domain domain = threadDomain.get();
    	return domain;
    }

    public static Company getCompany() {
    	Company company = (Company) threadCompany.get();
    	if (company != null) {
    		//company = CoreServiceLocator.getCompanyService().findCompany(company.getId());
	        HibernateUtil hibernate 
	        		= (HibernateUtil) CoreServiceLocator.findServiceBean("hibernate.util");

	        Session s = hibernate.createNewSession();
            company = (Company) s.load(Company.class, company.getId());
            s.close();
    	}
    	return company;
    }
    
    public static void closeSession() {
		if (log.isDebugEnabled()) {
			log.debug("closeSession() called");
		}
    	try {
        	HibernateTransaction transaction = threadTransaction.get();
        	if (transaction != null) {
        		if (transaction.getTransaction().isActive()) {
        			return;
        		}
        	}
    		
	    	Session s = hibernateSession.get();
	    	if (s != null) {
	    		if (s.isOpen()) {
		    		s.flush();
		    		if (log.isDebugEnabled())
		    			log.debug("Closing session for thread: " + Thread.currentThread().getName());
		    		s.close();
		    		synchronized(SESSION_COUNT_MUTEX) {
			    		openSessionCount--;
			    		sessionCountLog.debug("-------------- Session Closed   " + ThreadProperties.openSessionCount);
		    		}
	    		} else {
	    			sessionCountLog.info("Session is closed");
	    		}
	    	} else {
	    		sessionCountLog.info("Session is null, ignoring close request");
	    	}
    	} catch (Exception ex) {
    		log.error("Exception closing session", ex);
    		throw new SystemException(ex);
    	} finally {
    		hibernateSession.set(null);
    	}
    }

    /*
     * If the transaction is started, return a token,
     *  if the transaction is already in progress return null.
     */
    public static TransactionToken beginTransaction() {
    	TransactionToken token = null;
    	
    	Session session = getSession();
    	if (session == null) {
    		throw new SystemException("Session does not exist");
    	}
    	if (!session.isOpen()) {
    		throw new SystemException("Session is closed");
    	}
           	
    	HibernateTransaction newTransaction = threadTransaction.get();
    	//Transaction tx = (Transaction) threadTransaction.get();
    	try {
    		if (newTransaction == null) {
	    		log.debug("Starting transaction for thread: " + Thread.currentThread().getName());
    			newTransaction = new HibernateTransaction(getSession(), getSession().beginTransaction());
    			threadTransaction.set(newTransaction);
    			token = newTransaction.getToken();
    			if (!newTransaction.getSession().isOpen()) {
    				throw new SystemException("After starting transaction, session is not open");
    			}
    			if (!newTransaction.getSession().isConnected()) {
    				throw new SystemException("After starting transaction, session is not connected");
    			}
    		} else {
    			throw new SystemException("Transaction already started, cannot start transaction for thread: " 
    					+ Thread.currentThread().getName());
    			
    		}
    	} catch (Exception ex) {
    		throw new SystemException(ex);
    	}
    	
    	if (log.isDebugEnabled())
    		log.debug("beginTransaction() - Transaction = " + newTransaction);
    	
    	assertTransaction();
    	return(token);
    }
    
    public static void assertTransaction() {
    	assertSession();
    	
		HibernateTransaction transaction = threadTransaction.get();
    	if (log.isDebugEnabled())
    		log.debug("assertTransaction() - Transaction = " + transaction);
		if (transaction == null) {
			throw new SystemException("No transaction found for thread");
		}
		if (!transaction.getSession().isOpen()) {
    		throw new SystemException("Transaction's session is closed");
    	}
    	
    	if (transaction == null)
    		throw new SystemException("Transaction not in progress."
    		        + "  Assertion of existing transaction requested by caller. Thread: " 
    				+ Thread.currentThread().getName());
    	if (!transaction.getTransaction().isActive())
    		throw new SystemException("Transaction is not active."
    		        + "  Assertion of existing transaction requested by caller. Thread: " 
    				+ Thread.currentThread().getName());
		log.debug("Transaction active for (assertion ok) - Thread: " + Thread.currentThread().getName());
    	
    }
    
    public static void forcedCommitAndClose() {
    	//assertTransaction();\
    	HibernateTransaction transaction = threadTransaction.get();
		
    	if (log.isDebugEnabled())
			log.debug("forcedCommitAndClose() - Transaction = " + transaction);
    	
    	if (transaction != null) {
	    	if (transaction != null) {
	        	if (transaction.getSession().isOpen()) {
			    	if (transaction == null)
			    		throw new SystemException("Cannot find transaction to commit for thread: " 
			    				+ Thread.currentThread().getName());
			    	TransactionToken token = transaction.getToken();
		
			    	if (token == null)
			    		throw new SystemException("Transaction not started properly, no token found - thread: "
			    				+ Thread.currentThread().getName());
			    	if (!getSession().isOpen())
			    		throw new SystemException("Session is closed, cannot commit transaction");
			    	commitTransaction(token);
	        	} else {
	        		log.warn("Could not commit transaction because session was closed");
	        	}
	    	} else {
		    	closeSession();
	    	}
	    	threadTransaction.set(null);
    	//} else {
    	    //beginTransaction();
    	    //transaction = (HibernateTransaction)threadTransaction.get();
    	}
    	closeSession();
    }
    
    public static void forcedCommitAndNewTransaction() {
    	HibernateTransaction transaction = threadTransaction.get();
		
    	if (log.isDebugEnabled())
    		log.debug("forcedCommitAndNewSession() - Transaction = " + transaction);
    	
    	if (transaction != null) {
        	if (transaction.getSession().isOpen()) {
		    	TransactionToken token = transaction.getToken();
	
		    	if (token == null)
		    		throw new SystemException("Transaction not started properly,"
		    		        + " no token found - thread: "
		    				+ Thread.currentThread().getName());
		    	if (!getSession().isOpen())
		    		throw new SystemException("Session is closed, cannot commit transaction");
		    	commitTransaction(token);
		    	
		    	//closeSession();
		    	beginTransaction();
		    	transaction = threadTransaction.get();
		    	transaction.setToken(token);
        	} else {
        		throw new SystemException("Could not commit transaction, because owner session is closed");
        	}
    	} else {
    		closeSession();
	    	beginTransaction();
	    	transaction = threadTransaction.get();
    	}
    }
    
    public static void forcedCommit() {
    	/*assertTransaction();*/

    	HibernateTransaction transaction = threadTransaction.get();
		
    	if (log.isDebugEnabled())
    		log.debug("forcedCommit() - Transaction = " + transaction);
    	
    	if (transaction != null) {
        	Person person = getPerson();
	    	if (transaction == null)
	    		throw new SystemException("Cannot find transaction to commit for thread: " 
	    				+ Thread.currentThread().getName());
	    	TransactionToken token = transaction.getToken();
	    	if (token == null)
	    		throw new SystemException("Transaction not started properly, no token found - thread: "
	    				+ Thread.currentThread().getName());
	    	if (!getSession().isOpen())
	    		log.warn("Session is closed, cannot commit transaction");
	    	else {
		    	commitTransaction(token);
		    	beginTransaction();
		    	transaction = threadTransaction.get();
		    	transaction.setToken(token);
		    	if (person != null)
		    		setPerson(person);	
		    	//getSession().flush();
	    	}
	    }
    }
    
    @SuppressWarnings("unchecked")
    public static void commitTransaction(TransactionToken token) {
    	assertTransaction();
    	Session session = getSession();
    	
    	Set<Object> events = getEvents();
    	if ((events != null) && (events.size() > 0)) {
    		try {
    			Iterator<Object> iter = events.iterator();
    			while (iter.hasNext()) {
    				Event event = (Event) iter.next();
    				if (event.getId() != null) {
    					throw new SystemException("Event has already been saved, should not be attached to thread properties for save later");
    				}
    				
    				Iterator<Object> attachIter = event.getAttachments().iterator();
    				while (attachIter.hasNext()) {
    					EventAttachment attch = (EventAttachment) attachIter.next();
    					if (attch.getObjectId() == null) {
    						if (attch.getObject().getId() == null)
    							throw new SystemException("Event attachment has an object with a null id, make sure event object is saved");
    						attch.setObjectId(attch.getObject().getId().toString());
    					}
    				}
    				getSession().save(event);
    			}
    		} catch (Exception ex) {
    			throw new SystemException("Could not save events");
    		} finally {
    			events.clear();
    		}
    	}
    	
    	HibernateTransaction transaction = threadTransaction.get();
		
    	if (log.isDebugEnabled())
    		log.debug("commitTransaction() - Transaction = " + transaction);
		
    	Transaction tx = transaction.getTransaction();
    	try {
    		if (transaction != null) {
    			if (token != null) {
	    			if (transaction.getToken() == token) {
	    				if (session.isOpen()) {
		    		    	session.flush();
			    			if (!tx.wasCommitted()) {
			    				if (!tx.wasRolledBack()) {
			    					log.debug("Committing transaction, thread: " 
			    							+ Thread.currentThread().getName());
			    			    	assertTransaction();
		    			    		tx.commit();
			    				} else {
			    					throw new Exception("Attempt to commit a transaction"
                                            + " that was rolledback already. - thread: "
                                            + Thread.currentThread().getName());
			    				}
			    			} else {
								throw new Exception("Transaction was already committed"
                                        + " and attempt to commit again - thread: " + Thread.currentThread().getName());
			    			}
		    	    		threadTransaction.set(null);
	    				} else {
	    					log.warn("Could not commit because session is closed");
	    				}
	    			} else {
	    				throw new SystemException("Commit was requested by owner" + " with wrong token - thread: "
                                + Thread.currentThread().getName());
	    			}
    			} else {
    				log.debug("Token was null, for commit request, assuming"
                            + " that caller does not own transaction and" + " ignoring request, thread: "
                            + Thread.currentThread().getName());
    			}
    		} else {
				throw new Exception("Transaction is null and cannot be" + " committed - thread: "
                        + Thread.currentThread().getName());
    		}
    	} catch (Exception ex) {
    		log.error("Exception committing transaction - thread: "
    				+ Thread.currentThread().getName(), ex);
			if (!tx.wasRolledBack() ) {
				log.info("Rollback occurred");
				if (session.isOpen())
					rollbackTransaction();
			}
			throw new SystemException("Exception comitting transaction");
    	}
		threadTransaction.set(null);
    }
    
    public static void rollbackTransaction() {
    	HibernateTransaction transaction = threadTransaction.get();
    	log.warn("Rolling back transaction");
    	if (log.isDebugEnabled())
			log.debug("rollbackTransaction() - Transaction = " + transaction);
    	
		try {
    		threadTransaction.set(null);
    		if ( transaction != null) {
    			Transaction tx = transaction.getTransaction();
    			if (!tx.wasCommitted()) {
    				if (!tx.wasRolledBack() ) {
    	    			tx.rollback();
    				} else {
    					throw new SystemException("Transaction was already rolled-back before this request to roll-back.");
    				}
    			} else {
    				throw new SystemException("Transaction was already committed be for this request to roll-back");
    			}
    		} else {
    			log.debug("No transaction found to roll-back");
    		}
    	} catch (Exception ex) {
    		log.error("Exception rolling back transaction", ex);
    		throw new SystemException("Exception rolling back transaction", ex);
    	} finally {
    		getSession().close();
    	}
    }
}
